﻿using DXBase;
using DXBase.Structs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DXBase.D3D
{
	/// <summary>
	/// This class provide convenient way to maintain and edit an object transform model 
	/// in a way resilient to numerical error. The final transform is <c>Location * Rotation * Scale</c>.
	/// </summary>
	public class ModelTransform3D : ModelBase
	{
		public ModelTransform3D()
		{
			SetTransform(new Vector3F(), QuaternionF.Rotation(1, 0, 0, 0), new Vector3F(1, 1, 1));
		}

		public ModelTransform3D Clone()
		{
			var c = new ModelTransform3D();
			c.SetTransform(Location, Rotation, Scale);
			return c;
		}

		#region LeftHanded

		/// <summary>
		/// DirectX is, by default, Left coordinate system (i.e. the positive Z axis goes away from the viewer).
		/// Some model might be right coordinate system (math coordinate system)
		/// </summary>
		public bool LeftHanded
		{
			get { return mLeftHanded; }
			set
			{
				if (value == mLeftHanded)
					return;
				mLeftHanded = value;
				Transform = Transform3D.GetModel(Location, Rotation, Scale, LeftHanded);
				OnPropertyChanged();
			}
		}
		private bool mLeftHanded = true;

		#endregion

		#region Scale

		public Vector3F Scale
		{
			get { return mScale; }
			set
			{
				if (Equals(value, mScale))
					return;
				mScale = value;
				Transform = Transform3D.GetModel(Location, Rotation, Scale, LeftHanded);
				OnPropertyChanged();
			}
		}
		private Vector3F mScale;

		#endregion

		#region Rotation

		public QuaternionF Rotation
		{
			get { return mRotation; }
			set
			{
				if (Equals(value, mRotation))
					return;
				mRotation = value;
				Transform = Transform3D.GetModel(Location, Rotation, Scale, LeftHanded);
				OnPropertyChanged();
			}
		}
		private QuaternionF mRotation = QuaternionF.Rotation(1, 0, 0, 0);

		#endregion

		#region Location

		public Vector3F Location
		{
			get { return mLocation; }
			set
			{
				if (Equals(value, mLocation))
					return;
				mLocation = value;
				Transform = Transform3D.GetModel(Location, Rotation, Scale, LeftHanded);
				OnPropertyChanged();
			}
		}
		private Vector3F mLocation;

		#endregion

		#region Transform, NormalTransform, SetTransform()

		/// <summary>
		/// The transform Matrix to be used by DirectX
		/// </summary>
		public Matrix4x4F Transform
		{
			get { return mTransform; }
			private set
			{
				if (Equals(value, mTransform))
					return;
				mTransform = value;
				OnPropertyChanged();
			}
		}
		private Matrix4x4F mTransform;

		// TODO: Check that this works!!!

		/// <summary>
		/// The transform Matrix to be used by DirectX to transform normal vector
		/// </summary>
		public Matrix4x4F NormalTransform
		{
			get { return mNormalTransform; }
			private set
			{
				if (Equals(value, mNormalTransform))
					return;
				mNormalTransform = value;
				OnPropertyChanged();
			}
		}
		private Matrix4x4F mNormalTransform;

		/// <summary>
		/// Set all property in one go and calculate the transform only once.
		/// </summary>
		public void SetTransform(Vector3F position, QuaternionF rotation, Vector3F scale)
		{
			mLocation = position;
			mRotation = rotation;
			mScale = scale;
			Transform = Transform3D.GetModel(Location, Rotation, Scale, LeftHanded);
			NormalTransform = Transform3D.GetModel(new Vector3F(), Rotation, new Vector3F(1 / Scale.X, 1 / Scale.Y, 1 / Scale.Z), LeftHanded);
			OnPropertyChanged("Location");
			OnPropertyChanged("Rotation");
			OnPropertyChanged("Scale");
		}

		#endregion
	}

	public class CenteredRotationTransform
	{
		public CenteredRotationTransform()
		{
			Rotation = QuaternionF.Rotation(1, 0, 0, 0);
		}
		public CenteredRotationTransform(Vector3F origin, QuaternionF rotation)
		{
			Rotation = rotation;
			Origin = origin;
		}

		public Vector3F Origin 
		{
			get { return morigin; }
			set
			{
				morigin = value;
				Before =  -value;
				Update();
			}
		}
		Vector3F morigin;
		public Vector3F Before { get; private set; }
		public QuaternionF Rotation 
		{
			get { return mrotation; }
			set
			{
				mrotation = value;
				Update();
			}
		}
		QuaternionF mrotation;

		public Matrix4x4F Transform { get; private set; }
		void Update()
		{
			var m = Rotation.ToMatrix() * Matrix4x4F.Translation(Before);
			m.m03 += Origin.X;
			m.m13 += Origin.Y;
			m.m23 += Origin.Z;
			Transform = m;
		}

		/// <summary>
		/// Move the origin (will affect future rotation) without actually changing the current transform
		/// </summary>
		public void MoveOrigin(float x, float y, float z) { MoveOrigin(new Vector3F(x, y, z)); }
		/// <summary>
		/// Move the origin (will affect future rotation) without actually changing the current transform
		/// </summary>
		public void MoveOrigin(Vector3F o)
		{
			Before = (Rotation.Invert() * (o - Origin)) - Before;
			morigin = o;
			Update();
		}

		public void Apply(CenteredRotationTransform m) { Apply(m.Origin, m.Rotation); }
		public void Apply(Vector3F origin, QuaternionF rotation)
		{
			MoveOrigin(origin);
			Rotation = rotation * Rotation;
		}
		public CenteredRotationTransform Clone()
		{
			return new CenteredRotationTransform
			{
				morigin = Origin,
				mrotation = Rotation,
				Before = Before,
				Transform = Transform,
			};
		}
	}

	#region class Transforms: GetView(), GetModel(), GetProjection()

	public static class Transform3D
	{
		public static Matrix4x4F GetView(Vector3F eyePos, Vector3F lookV, Vector3F upV)
		{
			var vy = upV.Normalize();
			var vx = vy.Cross(lookV).Normalize();
			var vz = vx.Cross(vy).Normalize();
			var m = new Matrix4x4F(vx, vy, vz, eyePos);
			return m.Invert();
		}

		public static Matrix4x4F GetModel(Vector3F location, QuaternionF rotation, Vector3F scale, bool leftHanded = true)
		{
			if (leftHanded)
			{
				return Matrix4x4F.Translation(location) * rotation.ToMatrix() * Matrix4x4F.Scale(scale);
			}
			else
			{
				return Matrix4x4F.Translation(location) * rotation.Invert().ToMatrix() * Matrix4x4F.Scale(scale) * Matrix4x4F.Scale(1, 1, -1);
			}
		}

		public static Matrix4x4F GetProjection(float fieldOfView, DXContext ctxt, float nearPlane, float farPlane)
		{
			var ratio = ctxt.PixelSize.width / (float)ctxt.PixelSize.height;
			return GetProjection(fieldOfView, (float)ratio, nearPlane, farPlane);
		}
		public static Matrix4x4F GetProjection(float fieldOfView, float aspectRatio, float nearPlane, float farPlane)
		{
			float minScale = 1.0f / (float)Math.Tan(fieldOfView * Math.PI / 360.0f);
			float xScale = 1.0f;
			float yScale = 1.0f;
			if (aspectRatio < 1.0f)
			{
				xScale = minScale;
				yScale = minScale * aspectRatio;
			}
			else
			{
				xScale = minScale / aspectRatio;
				yScale = minScale;
			}
			float zScale = farPlane / (farPlane - nearPlane);
			return new Matrix4x4F(
				xScale, 0.0f, 0.0f, 0.0f,
				0.0f, yScale, 0.0f, 0.0f,
				0.0f, 0.0f, zScale, -nearPlane * zScale,
				0.0f, 0.0f, 1.0f, 0.0f
				);
		}
	}

	#endregion
}
